using System;
using System.Collections;
using System.Text;
using System.Drawing;
using System.Threading;
using System.Runtime.InteropServices;

using DirectShowLib;

namespace Pg.BioMedics.RatObserver.Media
{
    /// <summary>
    /// Frame buffer class used for buffered imge read from
    /// video stream. Utilized to both extract and DirectShow extraction graph management
    /// </summary>
    public sealed class FrameBuffer : IDisposable
    {
        #region Fields

        private readonly IGraphBuilder _GraphBuilder;
        private readonly IMediaControl _MediaControl;
        private readonly IVideoFrameStep _VideoFrameStep;
        private readonly IMediaEvent _MediaEvent;
        private readonly IMediaSeeking _MediaSeeking;
        private readonly SampleGrabberCB _SampleGrabber;

        private readonly ManualResetEvent _GraphEventNotifier;
        private FreameBufferState _State;

        private int _Capacity;
        private Queue _Frames;

        private int _frameIndex;

        #endregion

        #region Constructors and destructors

        /// <summary>
        /// Initializes FrameBuffer with DirectShow IGraphBuilder implementation object 
        /// and sample grabber utilized for direct bitmap extraction
        /// </summary>
        /// <param name="graphBuilder">DirectShow graph builder object</param>
        /// <param name="sampleGrabber">sample grabber object</param>
        /// <param name="capacity">frame buffer capacity</param>
        internal FrameBuffer(IGraphBuilder graphBuilder, SampleGrabberCB sampleGrabber, int capacity)
        {
            // input params validation
            if (graphBuilder == null)
                throw new ArgumentNullException("graphBuilder", "Can't initialize FrameBuffer, graph builder object can't be null");
            else if (sampleGrabber == null)
                throw new ArgumentNullException("grabber", "Can't initialize FrameBuffer, grabber object can't be null");
            else if (capacity <= 0)
                throw new ArgumentException(String.Format("Can't initialize FrameBuffer, capacity value is invalid {0} must be greater than 0", capacity), "capacity");

            try
            {
                // create local DirectSho control fields
                _GraphBuilder = graphBuilder;
                _MediaControl = graphBuilder as IMediaControl;
                _MediaEvent = graphBuilder as IMediaEvent;
                _MediaSeeking = graphBuilder as IMediaSeeking;
                _VideoFrameStep = graphBuilder as IVideoFrameStep;

                // get sample grabber
                _SampleGrabber = sampleGrabber;

                // create buffer queue
                _Capacity = capacity;
                _Frames = new Queue(capacity);

                // register events in sample grabber
                _SampleGrabber.OnFrameGrabbed += new FrameGrabbedDelegate(ExtractBitmap);

                // initialize graph watcher
                IntPtr eventHandle;
                int hr = _MediaEvent.GetEventHandle(out eventHandle);
                DsError.ThrowExceptionForHR(hr);

                _GraphEventNotifier = new ManualResetEvent(false);
                _GraphEventNotifier.SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(eventHandle, true);

                Thread eventWatchThread = new Thread(new ThreadStart(WatchGraphEvents));
                eventWatchThread.Start();
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't initialzie FrameBuffer", ex);
            }
        }

        /// <summary>
        /// Makes class uninitialization
        /// </summary>
        ~FrameBuffer()
        {
            Dispose();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Current playback progress
        /// </summary>
        public int Progress
        {
            get
            {
                try
                {
                    long stopTime = 0;
                    long currentTime = 0;

                    int hr = _MediaSeeking.GetPositions(out currentTime, out stopTime);
                    DsError.ThrowExceptionForHR(hr);

                    return (int)((currentTime * 100) / stopTime); // percentage progress
                }
                catch (Exception ex)
                {
                    throw new MediaException("Can't retrive extraction progress", ex);
                }
            }
        }

        /// <summary>
        /// Total frames in movie
        /// </summary>
        public int TotalFrames
        {
            get
            {
                try
                {                    
                    Guid currentTimeFormat = Guid.Empty;
                    long currentFrame = 0;
                    long stopFrame = 0;

                    int hr = _MediaSeeking.GetTimeFormat(out currentTimeFormat);
                    DsError.ThrowExceptionForHR(hr);

                    hr = _MediaSeeking.SetTimeFormat(TimeFormat.Frame);
                    DsError.ThrowExceptionForHR(hr);

                    hr = _MediaSeeking.GetPositions(out currentFrame, out stopFrame);
                    DsError.ThrowExceptionForHR(hr);

                    hr = _MediaSeeking.SetTimeFormat(currentTimeFormat);
                    DsError.ThrowExceptionForHR(hr);

                    return (int)stopFrame;
                }
                catch (Exception ex)
                {
                    throw new MediaException("Can't retrive total frames number", ex);
                }
            }
        }

        /// <summary>
        /// Current frame number
        /// </summary>
        public int CurrentFrame
        {
            get
            {
                try
                {
                    return _frameIndex;
                }
                catch (Exception ex)
                {
                    throw new MediaException("Can't retrive total frames number", ex);
                }
            }
        }

        /// <summary>
        /// Buffer capacity
        /// </summary>
        public int Capacity
        {
            get
            {
                return _Capacity;
            }
        }

        /// <summary>
        /// Number of bitmaps 
        /// </summary>
        public int Count
        {
            get
            {
                return _Frames.Count;
            }
        }

        /// <summary>
        /// Bitmaps in the frame buffer
        /// </summary>
        public Bitmap[] Bitmaps
        {
            get
            {
                Bitmap[] bitmaps = new Bitmap[_Frames.Count];
                object[] bitmapObjects = _Frames.ToArray();

                for (int i = 0; i < bitmapObjects.Length; i++)
                    bitmaps[i] = (Bitmap)bitmapObjects[i];

                return bitmaps;
            }
        }

        /// <summary>
        /// Bitmap from frame buffer
        /// </summary>
        /// <param name="index">image index</param>
        /// <returns>image</returns>
        public Bitmap this[int index]
        {
            get
            {
                return (Bitmap)_Frames.ToArray()[index];
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Get next frame from video and put it to the frame at last position
        /// remove first bitmap from the buffer.
        /// </summary>
        /// <returns>false - no bitmap to extract (end of video stream reached)</returns>
        public bool RequestFrame()
        {
            try
            {
                // TODO : passive mode implementation

                throw new NotImplementedException();
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't get next frame for frame buffer", ex);
            }
        }

        /// <summary>
        /// Extracts frames in active mode. After frame extraction an event is raised 
        /// </summary>
        public void RunActive()
        {
            try
            {
                int hr = _MediaControl.Run();
                DsError.ThrowExceptionForHR(hr);

                _State = FreameBufferState.Running;
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't run active frames extraction", ex);
            }
        }

        /// <summary>
        /// Stops active frames extraction
        /// </summary>
        public void StopActive()
        {
            try
            {
                int hr = _MediaControl.Stop();
                DsError.ThrowExceptionForHR(hr);

                _State = FreameBufferState.Stopped;

                if (OnStopActive != null)
                    OnStopActive.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't stop active frames extraction", ex);
            }
        }

        public void PauseActive()
        {
            try
            {
                int hr = _MediaControl.Pause();
                DsError.ThrowExceptionForHR(hr);

                _State = FreameBufferState.Paused;
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't stop active frames extraction", ex);
            }
        }

        /// <summary>
        /// Removes all frames currently stored in frame buffer
        /// </summary>
        public void Clear()
        {
            _Frames.Clear();

            // notify about changes
            if (OnChanged != null)
                OnChanged.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        /// Resets state of the frame buffer
        /// </summary>
        public void Reset()
        {
            try
            {
                // stop playback
                int hr = _MediaControl.Stop();
                DsError.ThrowExceptionForHR(hr);

                // move position to the beginning
                hr = _MediaSeeking.SetPositions(0, AMSeekingSeekingFlags.AbsolutePositioning,
                                                null, AMSeekingSeekingFlags.NoPositioning);
                DsError.ThrowExceptionForHR(hr);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't reset frame buffer state");
            }
        }

        #endregion

        #region Support methods

        /// <summary>
        /// Event handler invoked when a new image was extracted from
        /// video stream
        /// </summary>
        /// <returns>managed bitmap object</returns>
        private int ExtractBitmap(double sampleTime, Bitmap bitmap)
        {
            // add bitmap to frame buffer
            if (Count == Capacity)
                _Frames.Dequeue();

            _Frames.Enqueue(bitmap);
            _frameIndex++;

            // notify about frame buffer changes
            if (OnChanged != null)
                OnChanged.Invoke(this, EventArgs.Empty);

            return 0;
        }

        private void WatchGraphEvents()
        {
            do
            {
                // wait for event
                _GraphEventNotifier.WaitOne(-1, true);

                lock (this)
                {
                    if (_State == FreameBufferState.Running)
                    {
                        // get event
                        EventCode eventCode;
                        int lParam1, lParam2;
                        for (int hr = _MediaEvent.GetEvent(out eventCode, out lParam1, out lParam2, 0); hr >= 0;
                                 hr = _MediaEvent.GetEvent(out eventCode, out lParam1, out lParam2, 0))
                        {
                            // check event
                            if (eventCode == EventCode.Complete) // finished
                                StopActive();

                            // free event params
                            hr = _MediaEvent.FreeEventParams(eventCode, lParam1, lParam2);
                            DsError.ThrowExceptionForHR(hr);
                        }
                    }
                    else if (_State == FreameBufferState.Exiting) // unload graph watch thread
                        return;
                }
            }
            while (true);
        }

        #endregion

        #region Events

        public event EventHandler OnChanged;
        public event EventHandler OnStopActive;

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            _State = FreameBufferState.Exiting;

            if (_MediaControl != null)
                Marshal.ReleaseComObject(_MediaControl);

            if (_MediaEvent != null)
                Marshal.ReleaseComObject(_MediaEvent);

            if (_MediaSeeking != null)
                Marshal.ReleaseComObject(_MediaSeeking);

            if (_VideoFrameStep != null)
                Marshal.ReleaseComObject(_VideoFrameStep);

            if (_GraphBuilder != null)
                Marshal.ReleaseComObject(_GraphBuilder);

            // run GC
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        #endregion
    }

    public enum FreameBufferState
    {
        Stopped,
        Paused,
        Running,
        Exiting
    }
}
